/**
 * X509Certificate
 *
 * A representation for a X509 Certificate, with
 * methods to parse, verify and sign it.
 * Copyright (c) 2007 Henri Torgemane
 *
 * See LICENSE.txt for full license information.
 */
package com.hurlant.crypto.cert
{
	import com.hurlant.crypto.hash.IHash;
	import com.hurlant.crypto.hash.MD2;
	import com.hurlant.crypto.hash.MD5;
	import com.hurlant.crypto.hash.SHA1;
	import com.hurlant.crypto.rsa.RSAKey;
	import com.hurlant.util.ArrayUtil;
	import com.hurlant.util.Base64;
	import com.hurlant.util.der.ByteString;
	import com.hurlant.util.der.DER;
	import com.hurlant.util.der.OID;
	import com.hurlant.util.der.ObjectIdentifier;
	import com.hurlant.util.der.PEM;
	import com.hurlant.util.der.PrintableString;
	import com.hurlant.util.der.Sequence;
	import com.hurlant.util.der.Type;

	import flash.utils.ByteArray;

	public class X509Certificate
	{
		private var _loaded:Boolean;
		private var _param:*;
		private var _obj:Object;

		public function X509Certificate(p:*)
		{
			_loaded=false;
			_param=p;
			// lazy initialization, to avoid unnecessary parsing of every builtin CA at start-up.
		}

		private function load():void
		{
			if (_loaded)
				return;
			var p:*=_param;
			var b:ByteArray;
			if (p is String)
			{
				b=PEM.readCertIntoArray(p as String);
			}
			else if (p is ByteArray)
			{
				b=p;
			}
			if (b != null)
			{
				_obj=DER.parse(b, Type.TLS_CERT);
				_loaded=true;
			}
			else
			{
				throw new Error("Invalid x509 Certificate parameter: " + p);
			}
		}

		public function isSigned(store:X509CertificateCollection, CAs:X509CertificateCollection, time:Date=null):Boolean
		{
			load();
			// check timestamps first. cheapest.
			if (time == null)
			{
				time=new Date;
			}
			var notBefore:Date=getNotBefore();
			var notAfter:Date=getNotAfter();
			if (time.getTime() < notBefore.getTime())
				return false; // cert isn't born yet.
			if (time.getTime() > notAfter.getTime())
				return false; // cert died of old age.
			// check signature.
			var subject:String=getIssuerPrincipal();
			// try from CA first, since they're treated better.
			var parent:X509Certificate=CAs.getCertificate(subject);
			var parentIsAuthoritative:Boolean=false;
			if (parent == null)
			{
				parent=store.getCertificate(subject);
				if (parent == null)
				{
					return false; // issuer not found
				}
			}
			else
			{
				parentIsAuthoritative=true;
			}
			if (parent == this)
			{ // pathological case. avoid infinite loop
				return false; // isSigned() returns false if we're self-signed.
			}
			if (!(parentIsAuthoritative && parent.isSelfSigned(time)) && !parent.isSigned(store, CAs, time))
			{
				return false;
			}
			var key:RSAKey=parent.getPublicKey();
			return verifyCertificate(key);
		}

		public function isSelfSigned(time:Date):Boolean
		{
			load();

			var key:RSAKey=getPublicKey();
			return verifyCertificate(key);
		}

		private function verifyCertificate(key:RSAKey):Boolean
		{
			var algo:String=getAlgorithmIdentifier();
			var hash:IHash;
			var oid:String;
			switch (algo)
			{
				case OID.SHA1_WITH_RSA_ENCRYPTION:
					hash=new SHA1;
					oid=OID.SHA1_ALGORITHM;
					break;
				case OID.MD2_WITH_RSA_ENCRYPTION:
					hash=new MD2;
					oid=OID.MD2_ALGORITHM;
					break;
				case OID.MD5_WITH_RSA_ENCRYPTION:
					hash=new MD5;
					oid=OID.MD5_ALGORITHM;
					break;
				default:
					return false;
			}
			var data:ByteArray=_obj.signedCertificate_bin;
			var buf:ByteArray=new ByteArray;
			key.verify(_obj.encrypted, buf, _obj.encrypted.length);
			buf.position=0;
			data=hash.hash(data);
			var obj:Object=DER.parse(buf, Type.RSA_SIGNATURE);
			if (obj.algorithm.algorithmId.toString() != oid)
			{
				return false; // wrong algorithm
			}
			if (!ArrayUtil.equals(obj.hash, data))
			{
				return false; // hashes don't match
			}
			return true;
		}

		/**
		 * This isn't used anywhere so far.
		 * It would become useful if we started to offer facilities
		 * to generate and sign X509 certificates.
		 *
		 * @param key
		 * @param algo
		 * @return
		 *
		 */
		private function signCertificate(key:RSAKey, algo:String):ByteArray
		{
			var hash:IHash;
			var oid:String;
			switch (algo)
			{
				case OID.SHA1_WITH_RSA_ENCRYPTION:
					hash=new SHA1;
					oid=OID.SHA1_ALGORITHM;
					break;
				case OID.MD2_WITH_RSA_ENCRYPTION:
					hash=new MD2;
					oid=OID.MD2_ALGORITHM;
					break;
				case OID.MD5_WITH_RSA_ENCRYPTION:
					hash=new MD5;
					oid=OID.MD5_ALGORITHM;
					break;
				default:
					return null
			}
			var data:ByteArray=_obj.signedCertificate_bin;
			data=hash.hash(data);
			var seq1:Sequence=new Sequence;
			seq1[0]=new Sequence;
			seq1[0][0]=new ObjectIdentifier(0, 0, oid);
			seq1[0][1]=null;
			seq1[1]=new ByteString;
			seq1[1].writeBytes(data);
			data=seq1.toDER();
			var buf:ByteArray=new ByteArray;
			key.sign(data, buf, data.length);
			return buf;
		}

		public function getPublicKey():RSAKey
		{
			load();
			var pk:ByteArray=_obj.signedCertificate.subjectPublicKeyInfo.subjectPublicKey as ByteArray;
			pk.position=0;
			var rsaKey:Object=DER.parse(pk, [{name: "N"}, {name: "E"}]);
			return new RSAKey(rsaKey.N, rsaKey.E.valueOf());
		}

		/**
		 * Returns a subject principal, as an opaque base64 string.
		 * This is only used as a hash key for known certificates.
		 *
		 * Note that this assumes X509 DER-encoded certificates are uniquely encoded,
		 * as we look for exact matches between Issuer and Subject fields.
		 *
		 */
		public function getSubjectPrincipal():String
		{
			load();
			return Base64.encodeByteArray(_obj.signedCertificate.subject_bin);
		}

		/**
		 * Returns an issuer principal, as an opaque base64 string.
		 * This is only used to quickly find matching parent certificates.
		 *
		 * Note that this assumes X509 DER-encoded certificates are uniquely encoded,
		 * as we look for exact matches between Issuer and Subject fields.
		 *
		 */
		public function getIssuerPrincipal():String
		{
			load();
			return Base64.encodeByteArray(_obj.signedCertificate.issuer_bin);
		}

		public function getAlgorithmIdentifier():String
		{
			return _obj.algorithmIdentifier.algorithmId.toString();
		}

		public function getNotBefore():Date
		{
			return _obj.signedCertificate.validity.notBefore.date;
		}

		public function getNotAfter():Date
		{
			return _obj.signedCertificate.validity.notAfter.date;
		}

		public function getCommonName():String
		{
			var subject:Sequence=_obj.signedCertificate.subject;
			return (subject.findAttributeValue(OID.COMMON_NAME) as PrintableString).getString();
		}
	}
}

